<?php
/*
 *
 * @package MoltX
 * @author $Author$
 * @version $Id$
 * @link $URL$
 * @copyright Copyright (c) 2005-2010, Matthew Oltendorf
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 *
 */

/*
 * @ignore
 */
if ( ! defined ( 'blink' ) )
{
	// Access denied.
	header ( $_SERVER [ 'SERVER_PROTOCOL' ] . ' 403 Forbidden' );
	echo '403 - Forbidden';

	exit;
}

// Set the environment.
define ( 'blink_newline', "\n" );

/*
 * Master class
 * @package Blink
 *
 * Singleton
 */
final class blink
{
	// Internal switches.
	private static	$errors = false;

	// Default settings.
	public static	$architecture = 'polling',
					$cache = null,
					$cron = null,
					$formatting = null,
					$handler = null,
					$module = 'standalone',
					$output = 'json',
					$role = null,
					$room = null,
					$server = null, // Switch with storage when opening a background process.
					$storage = 'database', // Switch with server when opening a background process.
					$style = 'smoothness';

	/*
	 * Constructor function
	 *
	 * This is private to enforce singleton mode.
	 *
	 * @return null
	 */
	private function __construct ( )
	{
		return null;
	}

	/*
	 * Parse arguments function
	 *
	 * Convert arguments that were passed in on the command line to something
	 * more workable.
	 *
	 * @param array $input the parameters passed to the program in standard
	 *		commandline switch format.
	 *
	 * @param integer $start the position within the input to start at.
	 *
	 * @return array
	 */
	private static function arguments ( $input, $start = 1 )
	{
		$arguments = array ( );

		for ( $i = $start, $length = count ( $input ); $i < $length; ++$i )
		{
			$index = strpos ( $input [ $i ], '=' );

			if ( $index !== false )
			{
				$key = substr ( $input [ $i ], 1, $index - 1 );
				$value = substr ( $input [ $i ], $index + 1 );
			}
			else
			{
				$key = $input [ $i ];
				$value = true;
			}

			$arguments [ $key ] = $value;
		}
		//unset ( $index, $input, $key, $start, $value ); // No need to do this, we're in a function.

		foreach ( $arguments as $variable => &$value )
		{
			if ( ! property_exists ( __CLASS__, $variable ) )
			{
				unset ( $arguments [ $variable ] );

				continue;
			}

			// Assign data to self.
			self :: $$variable = &$value;
		}
		//unset ( $value ); // No need to do this, we're in a function.

		return $arguments;
	}

	/*
	 * Connect function.
	 *
	 * Connect to a server.
	 *
	 * @param string $role the role to connect to.
	 *
	 * @param boolean $spawn whether or not to automatically spawn the server
	 *		role if it is not running already, and then wait to connect to it.
	 *
	 * @return string
	 */
	public static function connect ( $role, $remote = false, $spawn = true )
	{
		$sock = ! $remote ? blink_storage . '/socket/' . $role . '.sock' : $role;

		// Do we have a handler?
		if ( ! $remote && ! file_exists ( $sock ) )
		{
			if ( $spawn )
			{
				self :: spawn ( $role );

				// Ensure we don't try to spawn it twice.
				$spawn = false;
			}
			else
			{
				return false;
			}
		}

		// Development only. @todo: Remove for production.
		blink_console :: log ( 'connecting to role' );

		// Try to connect...
		$server = blink_storage :: connect ( $role, $remote );

		// Development only. @todo: Remove for production.
		blink_console :: log ( 'connected to role' );

		if ( ! $server )
		{
			if ( ! $remote && $spawn )
			{
				self :: spawn ( $role );

				// Try to connect...
				$server = blink_storage :: connect ( $role, $remote );

				if ( ! $server )
				{
					return false;
				}
			}
			else
			{
				return false;
			}
		}

		return $server;
	}

	/*
	 * Errors function
	 *
	 * Set the error handlers.
	 *
	 * @param boolean $switch the switch to turn on or off the error/exception
	 *		handlers.
	 *
	 * @return null
	 */
	public static function errors ( $switch )
	{
		if ( $switch && ! self :: $errors )
		{
			// We have set this once.
			self :: $errors = true;

			set_error_handler ( array ( __CLASS__, 'error_handler' ) );
			set_exception_handler ( array ( __CLASS__, 'exception_handler' ) );
		}
		else if ( ! $switch && self :: $errors )
		{
			// We have unset this.
			self :: $errors = false;
		}

		return null;
	}

	/*
	 * Error handler function
	 *
	 * Handles an error.
	 *
	 * @param integer $type the type of the error.
	 *
	 * @param string $message the error message.
	 *
	 * @param string $file the file the error occured in.
	 *
	 * @param integer $line the line the error occured at.
	 *
	 * @param array $context all the variables available to the line of code the
	 *		error occured at.
	 *
	 * @return null
	 */
	public static function error_handler ( $type, $message, $file = null, $line = null, $context = null )
	{
		// Throw an ErrorException error to exception conversion exception.
		throw new ErrorException ( $message, 0, $type, $file, $line );

		// Don't execute internal PHP error handler.
		return true;
	}

	/*
	 * Exception handler function
	 *
	 * Handles an error.
	 *
	 * @param Exception $exception the exception object that was thrown.
	 *
	 * @return null
	 */
	public static function exception_handler ( $exception )
	{
		// Parse all data.
		$file = htmlentities ( $exception -> getFile ( ), ENT_QUOTES );
		$line = htmlentities ( $exception -> getLine ( ), ENT_QUOTES );
		$message = htmlentities ( $exception -> getMessage ( ), ENT_QUOTES );
		$output = htmlentities ( ob_get_clean ( ) );
		$trace = htmlentities ( $exception -> getTraceAsString ( ), ENT_QUOTES );

		// Output exception in a nice, fancy, easy-to-read form.
		echo include blink_system . '/exception.php';

		// Development only. @todo: Remove for production.
		blink_console :: log ( 'Exception Thrown: ' . $exception -> getMessage ( ) . "\n\t" . 'File: ' . $exception -> getFile ( ) . ' (' . $exception -> getLine ( ) . ')' . "\n\t" . 'Trace: ' . "\n\t\t" . str_replace ( "\n", "\n\t\t", $exception -> getTraceAsString ( ) ) );

		// Redundant.
		return true;
	}

	/*
	 * Initiate function
	 *
	 * Start the script.
	 *
	 * @param array $input the input that was passed on the commandline.
	 *
	 * @return null
	 */
	public static function initiate ( $input = null )
	{
		$data = self :: input ( );

		// Check if we were passed any arguments that override the configuration.
		if ( ! is_array ( $input ) )
		{
			// We were invoked by a client.
			define ( 'blink_browser', true );

			// Development only. @todo: Remove for production.
			if ( isset ( $_GET [ 'role' ] ) && $_GET [ 'role' ] === 'console' )
			{
				self :: $role = 'console';
			}
			else if ( ! count ( $data ) )
			{
				// Produce the main entry page.
				self :: template ( 'index' );

				return null;
			}
		}
		else
		{
			// We were invoked locally.
			define ( 'blink_browser', false );

			self :: arguments ( $input );
		}

		try
		{
			// Disable any default output buffering.
			for ( $i = ob_get_level ( ); $i > 0; --$i )
			{
				ob_end_clean ( );
			}
		}
		catch ( Exception $exception )
		{
			// Discard error.
		}

		if ( self :: $role !== 'console' )
		{
			// Start our own output buffering.
			ob_start ( );

			// Development only. @todo: Remove for production.
			include_once blink_system . '/console.php';

			// Development only. @todo: Remove for production.
			blink_console :: log ( 'starting' );

			switch ( self :: $storage )
			{
				case 'msg':
				case 'socket':
				case 'stream':
					if ( self :: $server === 'msg' || self :: $server === 'socket' || self :: $server == 'stream' )
					{
						throw new Exception ( 'INVALID CONFIGURATION' );
					}

					// Force passthrough module.
					self :: $module = 'passthrough';
				break;

				default:
					if ( self :: $module === 'passthrough' )
					{
						throw new Exception ( 'INVALID CONFIGURATION' );
					}

					// Load common files.
					include blink_system . '/user.php';
					include blink_system . '/session.php';
					include blink_system . '/room.php';
				break;
			}
		}
		else
		{
			self :: $module = 'passthrough';
		}

		// User aborts will not terminate the program.
		ignore_user_abort ( true );

		// Load module.
		include blink_system . '/module/' . self :: $module . '.php';

		blink_module :: initiate ( );

		if ( self :: $role !== 'console' )
		{
			// Development only. @todo: Remove for production.
			blink_console :: log ( 'ending' );

			// Start output.
			self :: output ( );
		}

		return null;
	}

	/*
	 * Parse input function
	 *
	 * Takes GET and POST info then converts it into an array.
	 *
	 * @return array
	 */
	public static function input ( )
	{
		static $rooms = null;

		if ( $rooms )
		{
			return $rooms;
		}

		$data = array ( );

		// Check if $_POST data is set.
		if ( isset ( $_POST [ 'data' ] ) )
		{
			$input = $_POST [ 'data' ];

			// Check if magic_quotes_gpc is on.
			if ( get_magic_quotes_gpc ( ) /* Fix for PHP 5.2.7 */ && version_compare ( PHP_VERSION, '5.2.7', '!=' ) )
			{
				$input = stripslashes ( $input );
			}

			$input = json_decode ( $input );

			if ( is_array ( $input ) )
			{
				$data = array_merge ( $data, $input );
			}
		}

		// Check if $_GET data is set.
		if ( isset ( $_GET [ 'data' ] ) )
		{
			$input = $_GET [ 'data' ];

			// Check if magic_quotes_gpc is on.
			if ( get_magic_quotes_gpc ( ) /* Fix for PHP 5.2.7 */ && version_compare ( PHP_VERSION, '5.2.7', '!=' ) )
			{
				$input = stripslashes ( $input );
			}

			$input = json_decode ( $input );

			if ( is_array ( $input ) )
			{
				$data = array_merge ( $data, $input );
			}
		}

		$rooms = array ( );

		// Clean all the data.
		foreach ( $data as $index => $room )
		{
			if ( ! is_object ( $room ) )
			{
				continue;
			}

			$sanitized = array ( );

			foreach ( $room as $type => $input )
			{
				switch ( $type )
				{
					case 'action':
					case 'channel':
					case 'level':
					case 'line':
						$sanitized [ $type ] = (integer) $input;
						break;

					case 'messages':
						if ( ! is_array ( $input ) )
						{
							$sanitized [ $type ] = array ( );
							break;
						}

						$sanitized [ $type ] = array ( );

						foreach ( $input as $value )
						{
							if ( ! is_array ( $value ) )
							{
								continue;
							}

							$message = array ( );

							foreach ( $value as $key => $descriptor )
							{
								switch ( $descriptor )
								{
									case 'content':
										$message [ $key ] = addslashes ( $descriptor );
										break;

									case 'transaction':
										$message [ $key ] = (integer) $descriptor;
										break;
								}
							}

							$sanitized [ $type ] [ ] = $message;
						}
						break;

					default:
						break;
				}
			}

			$rooms [ ] = $sanitized;
		}

		return $rooms;
	}

	/*
	 * Lock file function
	 *
	 * Lock onto a file.
	 *
	 * @param resource $pointer the file pointer resource to lock/unlock.
	 *
	 * @param boolean $set whether to lock, or unlock the file.
	 *
	 * @return boolean
	 */
	public static function lock ( $pointer, $set = true )
	{
		// Set the mode.
		$mode = $set ? LOCK_EX | LOCK_NB : LOCK_UN;

		// Wait up to 5 seconds to acquire the lock.
		for ( $i = 0; ! flock ( $pointer, $mode ); ++$i )
		{
			usleep ( 200000 );

			if ( $i < 25 )
			{
				continue;
			}

			return false;
		}

		return true;
	}

	/*
	 * Output data function
	 *
	 * Output all data.
	 *
	 * @return null
	 */
	public static function output ( )
	{
		// Development only. @todo: Remove for production.
		blink_console :: log ( 'contents: ' . ob_get_contents ( ) );

		// @todo: Add more functionality to blink :: output to improve performance.
		ob_end_flush ( );

		return null;
	}

	/*
	 * Spawn process function
	 *
	 * Spawn a new process.
	 *
	 * @param string $role the server role to spawn.
	 *
	 * @param string $room the room to spawn.
	 *
	 * @return null
	 */
	public static function spawn ( $role, $room = '' )
	{
		$room = $room ? $room . '.' : '';

		$lock = blink_storage . '/socket/' . $room . $role . '.lock';
		$sock = blink_storage . '/socket/' . $room . $role . '.sock';

		// Swap the server and the storage variables if $room is specified.
		$room = $room ? ' -room=' . substr ( $room, 0, -1 ) . ' -server=' . self :: $storage . ' -storage=' . self :: $server : '';

		$pointer = fopen ( $lock, 'a+' );

		// We need a special locking function for this scenario.
		// Wait up to 5 seconds to acquire the lock.
		for ( $i = 0; ! file_exists ( $sock ) && ! flock ( $pointer, LOCK_EX | LOCK_NB ); ++$i )
		{
			usleep ( 200000 );

			if ( $i < 25 )
			{
				continue;
			}

			flock ( $pointer, LOCK_UN );
			fclose ( $pointer );

			if ( ! file_exists ( $sock ) )
			{
				// Error.
				throw new Exception ( 'COULD NOT LOCK ' . strtoupper ( $role ) .' ROLE' );
			}
		}

		if ( ! file_exists ( $sock ) )
		{
			// Spawn a new process.
			if ( substr ( php_uname ( ), 0, 7 ) !== 'Windows' )
			{
				exec ( 'php "' . blink_application . '" -role=' . $role . $room . ' > /dev/null &' );
			}
			else
			{
				pclose ( popen ( 'start /B php "' . blink_application . '" -role=' . $role . $room, 'r' ) );
			}

			// Wait up to 5 seconds for the new process to spawn.
			for ( $i = 0; ! file_exists ( $sock ); ++$i )
			{
				usleep ( 200000 );

				if ( $i < 25 )
				{
					continue;
				}

				flock ( $pointer, LOCK_UN );
				fclose ( $pointer );

				// Error.
				throw new Exception ( 'COULD NOT SPAWN ' . strtoupper ( $role ) .' ROLE' );
			}
		}

		flock ( $pointer, LOCK_UN );
		fclose ( $pointer );

		return null;
	}

	/*
	 * Parse template function.
	 *
	 * @param string $file the template to read.
	 *
	 * @return null
	 */
	public static function template ( $file )
	{
		include blink_static . '/styles/' . self :: $style . '/' . $file . '.html';

		return null;
	}
}
